import collections
import contextlib
from pyson.json_encoding import register_cls
from pyson.pyson_collections.errors import *

class Locking(object):
    '''Locking mechanism for derived classes.'''

    def lock(self):
        '''Manually locks object to prevent changes'''

        self.__locked_size = True
        self.__locked_values = True

    def locksize(self):
        '''Prevent changes in object's size.'''

        self.__locked_size = True

    def lockvalues(self):
        '''Prevent changes in object's data. Can insert new entries, but cannot 
        not delete or change them.'''

        self.__locked_values = True

    def unlock(self):
        '''Manually unlocks list to allow changes'''

        self.__locked_size = False
        self.__locked_values = False

    def unlocksize(self):
        '''Allows changes in object's size'''

        self.__locked_size = False

    def unlockvalues(self):
        '''Allows changes in object's data'''

        self.__locked_values = False

    @property
    def is_locked(self):
        '''Return True if object is completely locked.'''

        return self.is_size_locked and self.is_values_locked

    @property
    def is_size_locked(self):
        '''Return True if object is locked for changes in size.'''

        try:
            return self.__locked_size
        except AttributeError:
            return True

    @property
    def is_values_locked(self):
        '''Return True if object is locked for changes in data.'''

        try:
            return self.__locked_values
        except AttributeError:
            return True

    @contextlib.contextmanager
    def unlocked(self):
        '''Context manager that unlocks list inside the with block and return
        to the previous state afterwards.
        '''

        prev = self.is_size_locked, self.is_values_locked
        self.unlock()
        try:
            yield
        finally:
            self.__locked_size, self.__locked_values = prev

    def _assure_unlocked(self):
        '''Raises an error if object is locked'''

        if self.is_locked:
            raise LockingError("trying to mutate locked '%s' object" % type(self).__name__)

    def _assure_size_unlocked(self):
        '''Raises an error if object is size-locked'''

        if self.is_locked:
            raise LockingError("trying to mutate locked '%s' object" % type(self).__name__)

    def _assure_values_unlocked(self):
        '''Raises an error if object is values-locked'''

        if self.is_locked:
            raise LockingError("trying to mutate locked '%s' object" % type(self).__name__)

#===============================================================================
# Locked base containers
#===============================================================================
@register_cls('pyson.lockedlist')
class lockedlist(list, Locking):
    '''
    A list that has a locking and unlocking mechanism in order to prevent/allow
    changes.
    
    Lists are created locked. Mutation can be enabled temporarily using the 
    `lockedlist.unlocked()` method in a ``with`` block. It is also possible to
    control the locked/unlocked state manually with the `lock()` and `unlock()`
    methods. 
    
    Examples
    --------
    
    Locked lists do not accept changes.
    
    >>> L = lockedlist([1,2,3,4]) 
    >>> del L[0]
    Traceback (most recent call last):
    ...
    LockingError: trying to mutate locked 'lockedlist' object
    
    However, using opening a with block with L.unlocked() temporarily 
    unlocks the list
    
    >>> with L.unlocked():
    ...     L[:] = [2,3,4,5]
    >>> L
    [2, 3, 4, 5]
    
    Locking and unlocking can also be done manually
    
    >>> L.unlock()
    >>> del L[0]; L
    [3, 4, 5]
    '''

    def __new__(cls, *args):
        new = list.__new__(cls, *args)
        return new

    # Mutator methods ----------------------------------------------------------
    def _keep_docstr(func): #@NoSelf
        # decorator that preserves the list's method docstring
        fname = func.__name__
        func.__doc__ = getattr(list, fname).__doc__
        return func

    @_keep_docstr
    def __delitem__(self, idx):
        self._assure_unlocked()
        list.__delitem__(self, idx)

    @_keep_docstr
    def __delslice__(self, i, j):
        self._assure_unlocked()
        list.__delslice__(self, i, j)

    @_keep_docstr
    def __iadd__(self, value):
        self._assure_size_unlocked()
        list.__iadd__(self, value)

    @_keep_docstr
    def __imul__(self, value):
        self._assure_size_unlocked()
        list.__imul__(self, value)

    @_keep_docstr
    def __setitem__(self, idx, value):
        self._assure_values_unlocked()
        list.__setitem__(self, idx, value)

    @_keep_docstr
    def __setslice__(self, i, j, y):
        self._assure_values_unlocked()
        list.__setslice__(self, i, j, y)

    @_keep_docstr
    def append(self, obj):
        self._assure_size_unlocked()
        list.append(self, obj)

    @_keep_docstr
    def extend(self, iterable):
        self._assure_size_unlocked()
        list.extend(self, iterable)

    @_keep_docstr
    def insert(self, index, obj):
        self._assure_unlocked()
        list.insert(self, index, obj)

    @_keep_docstr
    def pop(self, *args):
        self._assure_unlocked()
        return list.pop(self, *args)

    @_keep_docstr
    def remove(self, value):
        self._assure_unlocked()
        list.remove(self, value)

    @_keep_docstr
    def reverse(self):
        self._assure_values_unlocked()
        list.reverse(self)

    @_keep_docstr
    def sort(self, cmp=None, key=None, reverse=False): #@ReservedAssignment
        self._assure_values_unlocked()
        list.sort(self, cmp, key, reverse)

    del _keep_docstr

    # Json encoding ------------------------------------------------------------
    def __json_encode__(self):
        return list(self)

    @classmethod
    def __json_decode__(cls, obj):
        return cls(obj)

#===============================================================================
# Ad hoc locking of sequence types
#===============================================================================
class Locked(Locking):
    def __delitem__(self, idx):
        self._assure_unlocked()
        del self.data[idx]

    def __getitem__(self, idx):
        return self.data[idx]

    def __iter__(self):
        return iter(self.data)

    def __len__(self):
        return len(self.data)

    def __setitem__(self, idx, value):
        self._assure_values_unlocked()
        self.data[idx] = value

    def __json_encode__(self):
        return [self.data]

    @classmethod
    def __json_decode__(self, json):
        return type(self)(json[0])


@register_cls('pyson.LockedSeq')
class LockedSeq(Locked, collections.MutableSequence):
    '''Enables locking/unlocking mechanism in arbitrary sequences.
    
    LockedSeq works as a view for some sequence data that can be locked or 
    unlocked by the user. LockedSeq instances are created locked, and prevents 
    all changes:
     
    >>> data = [1, 2, 3]
    >>> L = LockedSeq(data)
    >>> L[:] = [4, 5, 6]
    Traceback (most recent call last):
    ...
    LockingError: trying to mutate locked 'LockedSeq' object
    
    It may be unlocked using the 'unlock' method or inside a ``with`` block: 
    
    >>> with L.unlocked():
    ...     L[:] = [4, 5, 6]
    
    The changes are propagated to original data object
     
    >>> L, data
    (LockedSeq([4, 5, 6]), [4, 5, 6])
    '''
    def __init__(self, seq):
        self.data = seq

    def insert(self, idx, value):
        self._assure_unlocked()
        self.data.insert(idx, value)

    def append(self, value):
        self._assure_size_unlocked()
        self.data.append(value)

    def __repr__(self):
        return 'LockedSeq([%s])' % (', '.join(repr(x) for x in self))

@register_cls('pyson.LockedSeq')
class LockedMap(Locked, collections.MutableMapping):
    '''Enables locking/unlocking mechanism in arbitrary mappings.
    
    LockedMap works as a view for some mapping data that can be locked or 
    unlocked by the user. LockedMap instances are created locked, and prevents 
    all changes:
     
    >>> data = {'foo': 'bar', 'spam': 'eggs'}
    >>> L = LockedMap(data)
    >>> L.update(ham='spam', bacon='eggs')
    Traceback (most recent call last):
    ...
    LockingError: trying to mutate locked 'LockedMap' object
    
    It may be unlocked using the 'unlock' method or inside a ``with`` block: 
    
    >>> with L.unlocked():
    ...     L['ham'] = 'spam'
    
    The changes are propagated to original data object
     
    >>> data == {'foo': 'bar', 'ham': 'spam', 'spam': 'eggs'}
    True
    '''
    def __init__(self, mapping):
        self.data = mapping

    def __repr__(self):
        return 'LockedMap({%s})' % (', '.join('%r: %r' % item for item in self.items()))

if __name__ == '__main__':
    import doctest
    doctest.testmod(optionflags=doctest.REPORT_ONLY_FIRST_FAILURE, verbose=0)


